Skip to content

fix(qemu): detect and repair partial toolchain cache entries, preflight shared libraries - #1296

Merged
zackees merged 3 commits into
mainfrom
fix/1266-qemu-libslirp-cache-corruption-defense
Aug 10, 2026
Merged

fix(qemu): detect and repair partial toolchain cache entries, preflight shared libraries#1296
zackees merged 3 commits into
mainfrom
fix/1266-qemu-libslirp-cache-corruption-defense

Conversation

@zackees

@zackees zackees commented Aug 10, 2026

Copy link
Copy Markdown
Member

Fixes #1266 — QEMU toolchain qemu-system-xtensa fails with libslirp.so.0: cannot open shared object file (exit 127).

Problem

When the cached Espressif QEMU toolchain is partial or corrupt (e.g. a CI cache restored from a crashed job before the atomic rename was committed, or an extract interrupted mid-flight), is_installed() reported true (the binary exists) but the binary couldn't find its bundled shared libraries at runtime. The emulator runner then reported a bare "exited with code 127" with no toolchain-path context.

Three defenses, stacked

  1. PackageBase::is_cached now requires the .install_complete sentinel. staged_install already extracts-to-staging-then-atomic-rename and writes the sentinel, so a directory without one is a partial or legacy cache entry. Treating it as not-cached triggers a clean re-extract.

  2. QEMU install validation verifies the bundled lib/ directory. The Espressif tarball ships bin/qemu-system-xtensa + lib/libslirp.so.0 with rpath $ORIGIN/../lib. validate_install_xtensa / validate_install_riscv32 now assert lib/ exists alongside the binary.

  3. Preflight probe before returning the resolved binary. On Linux, EspQemu::resolve_executable runs qemu --version before returning the path. Exit code 127 produces a diagnostic naming the missing library and the toolchain path, plus a rm -rf remediation. The emulator runner also detects exit 127 for defense-in-depth.

Tests

  • is_cached_returns_false_when_sentinel_is_missing — verifies the sentinel gate
  • bundled_libs_ok_standard_layout_bin_and_lib, bundled_libs_ok_alt_layout_top_level_with_lib, bundled_libs_missing_lib_dir_is_error, bundled_libs_binary_at_root_no_lib_dir_is_error — validate bundled lib/ detection
  • preflight_ok_when_binary_runs_version_successfully, preflight_linux_detects_missing_shared_library_exit_127 — preflight probe
  • Full fbuild-packages-fetch (131 tests) and fbuild-toolchain (131 tests) suites pass

Acceptance criteria

  • Two concurrent fbuild test-emu invocations sharing one cold QEMU toolchain cache key both start the emulator successfully — the install-lock + atomic rename already handle this; this PR adds the sentinel gate so a restored partial cache is rejected.
  • A partial/corrupt cached toolchain is detected and repaired rather than invoked — sentinel gate + lib/ validation catch this.
  • Emulator startup failure due to unresolved shared libraries produces a diagnostic naming the missing library and the toolchain path — preflight probe + exit-127 detection provide this.
  • FastLED can delete the libsdl2-2.0-0 install step from qemu_template.yml — the preflight catches missing system deps with an actionable message; the sentinel gate prevents partial extractions.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved QEMU startup errors with clearer guidance for incomplete or corrupted cached toolchains.
    • Added diagnostics for missing shared libraries, including steps to recover by removing the affected cache.
    • Cache detection now rejects incomplete installations instead of treating them as valid.
    • Toolchain validation now checks required bundled libraries for both supported architectures.
    • Completed staged installations are now consistently marked as ready.

…ht shared libraries

Fixes #1266 — QEMU toolchain libslirp.so.0: cannot open shared object file (exit 127).

Three defenses, stacked so each catches what the earlier misses:

1. **PackageBase::is_cached rejects installs without a sentinel.**
   staged_install already extracts-to-staging-then-atomic-rename and
   writes .install_complete, so a directory without one is a partial or
   legacy cache entry (CI cache restoration from an older fbuild, or a
   crash before the rename). Treating it as not-cached causes a clean
   re-extract rather than invoking a corrupt tree.

2. **QEMU install validation now verifies the bundled lib/ directory.**
   The Espressif tarball ships bin/qemu-system-xtensa and
   lib/libslirp.so.0 with rpath $ORIGIN/../lib. validate_install_*
    now asserts the lib/ directory exists alongside the binary, so a
   partial extract that somehow has the executable but lost the libs is
   caught at install-validation time (before the final rename).

3. **Preflight probe before returning the resolved binary.**
   On Linux, EspQemu::resolve_executable now runs qemu --version
   before handing the path back. Exit code 127 (dynamic linker failure)
   produces a diagnostic naming the missing library and the toolchain path,
   plus a rm -rf remediation line. The emulator runner also detects
   exit 127 and adds the same guidance as defense-in-depth.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@zackees, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: beac1067-8f80-4508-943d-1aa2d8b6b79b

📥 Commits

Reviewing files that changed from the base of the PR and between ff94af8 and 2aa1caa.

📒 Files selected for processing (1)
  • crates/fbuild-toolchain/src/toolchain/esp_qemu.rs
📝 Walkthrough

Walkthrough

QEMU toolchain validation now checks cache completion, bundled libraries, and Linux startup status. QEMU failures with exit code 127 report missing shared-library details and cached-toolchain recovery guidance.

Changes

QEMU toolchain integrity

Layer / File(s) Summary
Cache completion validation
crates/fbuild-packages-fetch/src/lib.rs
Cache hits now require an install directory and .install_complete. Tests cover incomplete and completed installs.
QEMU layout and startup preflight
crates/fbuild-toolchain/src/toolchain/esp_qemu.rs
QEMU resolution validates bundled lib/ directories and runs a Linux --version preflight. Tests cover valid layouts, missing libraries, and probe results.
Startup error diagnostics
crates/fbuild-daemon/src/handlers/emulator/shared.rs
Exit code 127 now reports missing shared libraries, runtime dependency guidance, and cached-toolchain removal instructions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Package cache
  participant QEMU toolchain resolver
  participant QEMU process handler
  Package cache->>QEMU toolchain resolver: provide complete cached toolchain
  QEMU toolchain resolver->>QEMU toolchain resolver: validate bundled lib/ directory
  QEMU toolchain resolver->>QEMU process handler: run QEMU startup preflight
  QEMU process handler-->>QEMU toolchain resolver: return startup status
  QEMU process handler-->>Package cache: report exit 127 and cache-removal guidance
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR addresses cache validation, bundled libraries, preflight, and exit 127 diagnostics, but provides no evidence for concurrent cold-cache extraction or workaround removal [#1266]. Add concurrency coverage or atomic extraction, and confirm that consumers can remove the documented QEMU runtime dependency workaround.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: repairing partial QEMU cache entries and preflighting shared libraries.
Out of Scope Changes check ✅ Passed The changes focus on QEMU cache integrity, bundled library validation, startup preflight, and related diagnostics required by issue #1266.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1266-qemu-libslirp-cache-corruption-defense

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fbuild-packages-fetch/src/lib.rs`:
- Around line 273-292: The staged_install flow must repair incomplete existing
installs instead of returning them. Before atomically renaming the staging
directory, write the install-complete sentinel and propagate any write failure
as an installation error; under the install lock, remove an existing install
directory when is_cached reports it incomplete, then reinstall it. Add a
regression test covering staged_install with a pre-existing directory missing
the sentinel.

In `@crates/fbuild-toolchain/src/toolchain/esp_qemu.rs`:
- Around line 127-131: Update the cached-install branch in EspQemu’s resolver
around is_installed to call qemu_validate_bundled_libs before accepting the QEMU
path, including the macOS path that currently skips preflight. When validation
fails, route the result through the existing cache-repair flow instead of
returning the broken cached path.
- Around line 240-255: The QEMU diagnostics derive unsafe cache deletion paths
from the executable location. In
crates/fbuild-toolchain/src/toolchain/esp_qemu.rs lines 240-255, carry the
fbuild-owned cache root into the diagnostic and emit a deletion command only for
that exact managed cache entry; omit deletion guidance for external paths
resolved through environment variables or PATH. In
crates/fbuild-daemon/src/handlers/emulator/shared.rs lines 361-370, remove
executable-parent cleanup guidance, retain the executable path, and show cache
cleanup only when a verified fbuild cache root is available.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ef000bb-0bb8-48a2-9151-d878b9972699

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0cf68 and ff94af8.

📒 Files selected for processing (3)
  • crates/fbuild-daemon/src/handlers/emulator/shared.rs
  • crates/fbuild-packages-fetch/src/lib.rs
  • crates/fbuild-toolchain/src/toolchain/esp_qemu.rs

Comment on lines +273 to +292
///
/// Requires both the install directory AND the `.install_complete` sentinel
/// to be present. A directory without the sentinel is an incomplete or
/// partial install (e.g. a CI cache restored from a crashed job before the
/// atomic rename was committed, or an extract interrupted mid-flight). The
/// caller should treat this as "not installed" so the package is
/// re-extracted rather than invoked with a corrupt tree.
///
/// On a cache hit, bumps the LRU timestamp in the DiskCache index.
pub fn is_cached(&self) -> bool {
let path = self.install_path();
let cached = path.exists() && path.is_dir();
if cached {
self.touch_disk_cache();
if !path.exists() || !path.is_dir() {
return false;
}
cached
let sentinel = disk_cache::paths::install_complete_sentinel(&path);
if !sentinel.exists() {
return false;
}
self.touch_disk_cache();
true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Repair incomplete cache directories before returning them.

At Line 344, staged_install returns an existing install directory before it validates or replaces it. The new predicate reports a missing sentinel as a cache miss, but the retry returns the same incomplete directory.

Write the sentinel in staging before the atomic rename. Treat a sentinel write failure as an install failure. Under the install lock, remove and reinstall an existing directory that does not meet the completeness requirement. Add a regression test that calls staged_install against an existing directory without the sentinel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-packages-fetch/src/lib.rs` around lines 273 - 292, The
staged_install flow must repair incomplete existing installs instead of
returning them. Before atomically renaming the staging directory, write the
install-complete sentinel and propagate any write failure as an installation
error; under the install lock, remove an existing install directory when
is_cached reports it incomplete, then reinstall it. Add a regression test
covering staged_install with a pre-existing directory missing the sentinel.

Source: Coding guidelines

Comment on lines +127 to +131
} else if self.is_installed() {
let path = find_qemu_binary(&self.base.install_path(), self.arch)?;
hydrate_windows_runtime(&path)?;
validate_windows_runtime(&path)?;
return Ok(path);
}

if let Some(path) = find_existing_idf_qemu(self.arch) {
path

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Validate bundled libraries before accepting a cached QEMU installation.

EspQemu::is_installed checks for a sentinel and executable, but it does not call qemu_validate_bundled_libs. A cache restore can preserve those two items while omitting lib/.

On macOS, Lines 215-219 skip the preflight. The resolver then returns a broken cached QEMU path. Include bundled-library validation in the cached-install path, and route a failed validation through cache repair.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-toolchain/src/toolchain/esp_qemu.rs` around lines 127 - 131,
Update the cached-install branch in EspQemu’s resolver around is_installed to
call qemu_validate_bundled_libs before accepting the QEMU path, including the
macOS path that currently skips preflight. When validation fails, route the
result through the existing cache-repair flow instead of returning the broken
cached path.

Comment on lines +240 to +255
Err(FbuildError::PackageError(format!(
"QEMU at {} cannot start: a required shared library is missing.\n\
{}\n\
The cached QEMU toolchain appears incomplete or corrupt.\n\
To fix, delete the cached toolchain and retry:\n rm -rf {}",
qemu_binary.display(),
missing.as_deref().unwrap_or(&format!(
"The dynamic linker reported: {}",
stderr.trim()
)),
qemu_binary
.parent()
.and_then(|p| p.parent())
.unwrap_or(qemu_binary.parent().unwrap_or(qemu_binary))
.display(),
)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not derive cache deletion commands from the executable path.

QEMU can resolve from an environment variable or PATH, not only from the fbuild cache. For /usr/bin/qemu-system-xtensa, the toolchain diagnostic suggests rm -rf /usr, and the daemon diagnostic suggests deletion of /usr/bin.

  • crates/fbuild-toolchain/src/toolchain/esp_qemu.rs#L240-L255: Carry the fbuild-owned cache root into the diagnostic. Only show a deletion command for that exact managed cache entry. Omit deletion guidance for external QEMU paths.
  • crates/fbuild-daemon/src/handlers/emulator/shared.rs#L361-L370: Remove executable-parent cleanup guidance. Report the executable path, but show cache cleanup only when a verified fbuild cache root is available.
📍 Affects 2 files
  • crates/fbuild-toolchain/src/toolchain/esp_qemu.rs#L240-L255 (this comment)
  • crates/fbuild-daemon/src/handlers/emulator/shared.rs#L361-L370
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-toolchain/src/toolchain/esp_qemu.rs` around lines 240 - 255,
The QEMU diagnostics derive unsafe cache deletion paths from the executable
location. In crates/fbuild-toolchain/src/toolchain/esp_qemu.rs lines 240-255,
carry the fbuild-owned cache root into the diagnostic and emit a deletion
command only for that exact managed cache entry; omit deletion guidance for
external paths resolved through environment variables or PATH. In
crates/fbuild-daemon/src/handlers/emulator/shared.rs lines 361-370, remove
executable-parent cleanup guidance, retain the executable path, and show cache
cleanup only when a verified fbuild cache root is available.

zackees and others added 2 commits August 9, 2026 20:51
The preflight used bare std::process::Command::new which (a) flashes a
console window on Windows daemon-hosted emulator runs and (b) trips the
allow-direct-spawn lint gate. Switch to fbuild_core::subprocess::
run_command_blocking which routes through containment on all platforms.

Co-Authored-By: Claude <noreply@anthropic.com>
The return Ok(()); in the #[cfg(not(target_os = "linux"))]
branch of preflight_qemu_binary is the last statement in the function
on macOS, triggering clippy's needless_return lint (denied by
-D warnings). Replace with a plain tail expression.

Co-Authored-By: Claude <noreply@anthropic.com>
@zackees
zackees merged commit 067732d into main Aug 10, 2026
92 checks passed
@zackees
zackees deleted the fix/1266-qemu-libslirp-cache-corruption-defense branch August 10, 2026 04:21
@fastled-project-sync fastled-project-sync Bot moved this to Triage in FastLED Tracker Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

QEMU toolchain: qemu-system-xtensa fails with 'libslirp.so.0: cannot open shared object file' (exit 127)

1 participant